/*
 *
 *    Copyright (c) 2020-2021 Project CHIP Authors
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

// Import helpers from zap core
const templateUtil = require('../../../../../../generator/template-util');
const zclHelper = require('../../../../../../generator/helper-zcl');
const iteratorUtil = require('../../../../../../util/iterator-util');
const queryAccess = require('../../../../../../db/query-access');
const queryZcl = require('../../../../../../db/query-zcl');
const dbEnum = require('../../../../../../../src-shared/db-enum');

const { asBlocks, ensureClusters } = require('../../common/ClustersHelper');

function throwErrorIfUndefined(item, errorMsg, conditions) {
  conditions.forEach((condition) => {
    if (condition == undefined) {
      console.log(item);
      console.log(errorMsg);
      throw error;
    }
  });
}

function checkIsInsideClusterBlock(context, name) {
  const clusterName = context.name ? context.name : context.clusterName;
  const clusterSide = context.side ? context.side : context.clusterSide;
  const errorMsg = name + ': Not inside a ({#chip_server_clusters}} block.';
  throwErrorIfUndefined(context, errorMsg, [clusterName, clusterSide]);

  return { clusterName, clusterSide };
}

function checkIsInsideCommandBlock(context, name) {
  const clusterName = context.clusterName;
  const clusterSide = context.clusterSide;
  const commandId = context.id;
  const errorMsg = name + ': Not inside a ({#chip_cluster_commands}} block.';

  throwErrorIfUndefined(context, errorMsg, [
    commandId,
    clusterName,
    clusterSide,
  ]);

  return commandId;
}

function checkIsInsideAttributeBlock(context, name) {
  const code = context.code;
  const errorMsg = name + ': Not inside a ({#chip_server_attributes}} block.';

  throwErrorIfUndefined(context, errorMsg, [code]);
}

function checkIsChipType(context, name) {
  const type = context.chipType;
  const errorMsg = name + ': Could not find chipType';

  throwErrorIfUndefined(context, errorMsg, [type]);

  return type;
}

function getCommands(methodName) {
  const { clusterName, clusterSide } = checkIsInsideClusterBlock(
    this,
    methodName
  );
  return clusterSide == 'client'
    ? ensureClusters(this).getClientCommands(clusterName)
    : ensureClusters(this).getServerCommands(clusterName);
}

function getAttributes(methodName) {
  const { clusterName, clusterSide } = checkIsInsideClusterBlock(
    this,
    methodName
  );
  return ensureClusters(this).getAttributesByClusterName(clusterName);
}

function getResponses(methodName) {
  const { clusterName, clusterSide } = checkIsInsideClusterBlock(
    this,
    methodName
  );
  return clusterSide == 'client'
    ? ensureClusters(this).getClientResponses(clusterName)
    : ensureClusters(this).getServerResponses(clusterName);
}

/**
 * Creates block iterator over the enabled server side clusters
 *
 * @param {*} options
 */
function chip_server_clusters(options) {
  return asBlocks.call(
    this,
    ensureClusters(this, options.hash.includeAll).getServerClusters(),
    options
  );
}

/**
 * Check if there is any enabled server clusters
 *
 */
function chip_has_server_clusters(options) {
  return ensureClusters(this)
    .getServerClusters()
    .then((clusters) => !!clusters.length);
}

/**
 * Creates block iterator over client side enabled clusters
 *
 * @param {*} options
 */
function chip_client_clusters(options) {
  return asBlocks.call(
    this,
    ensureClusters(this, options.hash.includeAll).getClientClusters(),
    options
  );
}

/**
 * Check if there is any enabled client clusters
 *
 */
function chip_has_client_clusters(options) {
  return ensureClusters(this)
    .getClientClusters()
    .then((clusters) => !!clusters.length);
}

/**
 * Creates block iterator over enabled clusters
 *
 * @param {*} options
 */
function chip_clusters(options) {
  return asBlocks.call(
    this,
    ensureClusters(this, options.hash.includeAll).getClusters(),
    options
  );
}

/**
 * Check if there is any enabled clusters
 *
 */
function chip_has_clusters(options) {
  return ensureClusters(this)
    .getClusters()
    .then((clusters) => !!clusters.length);
}

/**
 * Creates block iterator over the server global responses
 *
 * @param {*} options
 */
function chip_server_global_responses(options) {
  return asBlocks.call(this, getServerGlobalAttributeResponses(this), options);
}

async function if_basic_global_response(options) {
  const attribute = this.response.arguments[0];
  const globalResponses = await getServerGlobalAttributeResponses(this);
  const complexType =
    attribute.isNullable ||
    attribute.isOptional ||
    attribute.isStruct ||
    attribute.isArray;
  const responseTypeExists = globalResponses.find(
    (item) => item.chipType == attribute.chipType
  );

  if (!complexType && responseTypeExists) {
    return options.fn(this);
  } else {
    return options.inverse(this);
  }
}

function getServerGlobalAttributeResponses(context) {
  const sorter = (a, b) =>
    a.chipCallback.name.localeCompare(b.chipCallback.name, 'en', {
      numeric: true,
    });

  const reducer = (unique, item) => {
    const {
      type,
      size,
      isArray,
      isOptional,
      isNullable,
      chipCallback,
      chipType,
    } = item.response.arguments[0];

    // List-typed elements have a dedicated callback
    if (isArray) {
      return unique;
    }

    if (unique.find((item) => item.chipCallback.name == chipCallback.name)) {
      return unique;
    }

    return [
      ...unique,
      { chipCallback, chipType, size, isOptional, isNullable },
    ];
  };

  const filter = (attributes) => attributes.reduce(reducer, []).sort(sorter);
  return ensureClusters(context)
    .getAttributesByClusterSide('server')
    .then(filter);
}

/**
 * Creates block iterator over the cluster commands for a given cluster/side.
 *
 * This function is meant to be used inside a {{#chip_*_clusters}}
 * block. It will throw otherwise.
 *
 * @param {*} options
 */
function chip_cluster_commands(options) {
  const commands = getCommands.call(this, 'chip_cluster_commands');

  return asBlocks.call(this, commands, options);
}

/**
 * Creates block iterator over the cluster responses for a given cluster/side.
 *
 * This function is meant to be used inside a {{#chip_*_clusters}}
 * block. It will throw otherwise.
 *
 * @param {*} options
 */
function chip_cluster_responses(options) {
  const responses = getResponses.call(this, 'chip_cluster_responses');

  return asBlocks.call(this, responses, options);
}

/**
 * Creates block iterator over the current command arguments for a given cluster/side.
 *
 * This function is meant to be used inside a {{#chip_cluster_commands}}
 * block. It will throw otherwise.
 *
 * @param {*} options
 */
function chip_cluster_command_arguments(options) {
  const commandId = checkIsInsideCommandBlock(
    this,
    'chip_cluster_command_arguments'
  );
  const commands = getCommands.call(
    this.parent,
    'chip_cluster_commands_argments'
  );

  const filter = (command) => command.id == commandId;
  return asBlocks.call(
    this,
    commands.then((items) => items.find(filter).arguments),
    options
  );
}

/**
 * Creates block iterator over the current command arguments for a given cluster/side.
 *
 * This function is meant to be used inside a {{#chip_cluster_commands}}
 * block. It will throw otherwise.
 *
 * The arguments list built by this function differs from {{chip_cluster_command_arguments}}.
 * For example, if a command contains a single struct argument "SomeStruct", with the following type:
 *
 * struct SomeStruct {
 *   uint8_t a;
 *   uint16_t b;
 *   uint32_t c;
 * }
 *
 * then that argument will be expanded into 3 arguments (uint8_t a, uint16_t b, uint32_t c).
 *
 * @param {*} options
 */
function chip_cluster_command_arguments_with_structs_expanded(options) {
  const commandId = checkIsInsideCommandBlock(
    this,
    'chip_cluster_command_arguments'
  );
  const commands = getCommands.call(
    this.parent,
    'chip_cluster_command_arguments_with_structs_expanded'
  );

  const filter = (command) => command.id == commandId;
  return asBlocks.call(
    this,
    commands.then((items) => {
      const item = items.find(filter);
      if (item === undefined) {
        return [];
      }
      return item.expandedArguments || item.arguments;
    }),
    options
  );
}

/**
 * Creates block iterator over the current response arguments for a given cluster/side.
 *
 * This function is meant to be used inside a {{#chip_cluster_responses}}
 * block. It will throw otherwise.
 *
 * @param {*} options
 */
function chip_cluster_response_arguments(options) {
  const commandId = checkIsInsideCommandBlock(
    this,
    'chip_cluster_response_arguments'
  );
  const responses = getResponses.call(
    this.parent,
    'chip_cluster_responses_argments'
  );

  const filter = (command) => command.id == commandId;
  return asBlocks.call(
    this,
    responses.then((items) => items.find(filter).arguments),
    options
  );
}

/**
 * Returns if a given server cluster has any attributes of type List[T]
 *
 * This function is meant to be used inside a {{#chip_server_clusters}}
 * block. It will throw otherwise.
 *
 * @param {*} options
 */
function chip_server_has_list_attributes(options) {
  const { clusterName } = checkIsInsideClusterBlock(
    this,
    'chip_server_has_list_attributes'
  );
  const attributes = ensureClusters(this).getServerAttributes(clusterName);

  const filter = (attribute) => attribute.isArray;
  return attributes.then((items) => items.find(filter));
}

/**
 * Returns if a given client cluster has any attributes of type List[T]
 *
 * This function is meant to be used inside a {{#chip_client_clusters}}
 * block. It will throw otherwise.
 *
 * @param {*} options
 */
function chip_client_has_list_attributes(options) {
  const { clusterName } = checkIsInsideClusterBlock(
    this,
    'chip_client_has_list_attributes'
  );
  const attributes = ensureClusters(this).getClientAttributes(clusterName);

  const filter = (attribute) => attribute.isArray;
  return attributes.then((items) => items.find(filter));
}

/**
 * Returns if a given server cluster has any reportable attribute
 *
 * This function is meant to be used inside a {{#chip_server_clusters}}
 * block. It will throw otherwise.
 *
 * @param {*} options
 */
function chip_server_has_reportable_attributes(options) {
  const { clusterName } = checkIsInsideClusterBlock(
    this,
    'chip_server_has_reportable_attributes'
  );
  const attributes = ensureClusters(this).getServerAttributes(clusterName);

  const filter = (attribute) => attribute.isReportableAttribute;
  return attributes.then((items) => items.find(filter));
}

/**
 * Creates block iterator over the server side cluster attributes
 * for a given cluster.
 *
 * This function is meant to be used inside a {{#chip_server_clusters}}
 * block. It will throw otherwise.
 *
 * @param {*} options
 */
function chip_server_cluster_attributes(options) {
  const { clusterName } = checkIsInsideClusterBlock(
    this,
    'chip_server_cluster_attributes'
  );
  const attributes = ensureClusters(this).getServerAttributes(clusterName);

  return asBlocks.call(this, attributes, options);
}

/**
 * Creates block iterator over the server side cluster events
 * for a given cluster.
 *
 * This function is meant to be used inside a {{#chip_server_clusters}}
 * block. It will throw otherwise.
 *
 * @param {*} options
 */
function chip_server_cluster_events(options) {
  const { clusterName } = checkIsInsideClusterBlock(
    this,
    'chip_server_cluster_events'
  );
  const events = ensureClusters(this).getServerEvents(clusterName);

  return asBlocks.call(this, events, options);
}

function chip_attribute_list_entryTypes(options) {
  checkIsInsideAttributeBlock(this, 'chip_attribute_list_entry_types');
  return templateUtil.collectBlocks(this.items, options, this);
}

/**
 * Creates block iterator over commands for a given cluster that have the
 * following properties:
 *
 * 1) Are not manufacturer-specific (to exclude MfgSpecificPing)
 * 2) Are available in the isCommandAvailable sense.
 */
function chip_available_cluster_commands(options) {
  const { clusterName, clusterSide } = checkIsInsideClusterBlock(
    this,
    'chip_available_cluster_commands'
  );
  let promise = iteratorUtil.all_user_cluster_commands_helper
    .call(this, options)
    .then((endpointCommands) =>
      endpointCommands.filter((command) => {
        return (
          command.clusterName == clusterName &&
          zclHelper.isCommandAvailable(
            clusterSide,
            command.incoming,
            command.outgoing,
            command.commandSource,
            command.name
          ) &&
          /* exclude MfgSpecificPing */ !command.mfgCode
        );
      })
    )
    .then((filteredCommands) =>
      templateUtil.collectBlocks(filteredCommands, options, this)
    );
  return promise;
}

/**
 * Creates block iterator over structures belonging to the current cluster
 */
async function chip_cluster_specific_structs(options) {
  const { clusterName, clusterSide } = checkIsInsideClusterBlock(
    this,
    'chip_cluster_specific_structs'
  );

  const structs = await ensureClusters(this).getStructuresByClusterName(
    clusterName
  );

  return templateUtil.collectBlocks(structs, options, this);
}

/**
 * Creates block iterator over structures that are shared between clusters
 */
async function chip_shared_structs(options) {
  const structs = await ensureClusters(this).getSharedStructs();
  return templateUtil.collectBlocks(structs, options, this);
}

async function chip_endpoints(options) {
  const endpoints = await ensureClusters(this).getEndPoints();
  return templateUtil.collectBlocks(endpoints, options, this);
}

async function chip_endpoint_clusters(options) {
  const clusters = this.clusters;
  return templateUtil.collectBlocks(clusters, options, this);
}

/**
 * Helper checks if the type for the bitmap is BitFlags. This generally includes
 * all bitmaps apart from
 * bitmap8/16/32 (generally defined in types.xml)
 * example:
 * {{#if_is_strongly_typed_bitmap type}}
 * strongly typed bitmap
 * {{else}}
 * not a strongly typed bitmap
 * {{/if_is_strongly_typed_bitmap}}
 *
 * @param {*} type
 * @returns Promise of content.
 */
async function if_is_strongly_typed_bitmap(type, options) {
  let packageIds = await templateUtil.ensureZclPackageIds(this);
  let bitmap;
  if (type && typeof type === 'string') {
    bitmap = await queryZcl.selectBitmapByName(
      this.global.db,
      packageIds,
      type
    );
  } else {
    bitmap = await queryZcl.selectBitmapById(this.global.db, type);
  }

  if (bitmap) {
    let a = await queryZcl.selectAtomicType(
      this.global.db,
      packageIds,
      bitmap.name
    );
    if (a) {
      // If this is an atomic type, it's a generic, weakly typed, bitmap.
      return options.inverse(this);
    } else {
      return options.fn(this);
    }
  }
  return options.inverse(this);
}

/**
 * Handlebar helper function which checks if an enum is a strongly typed enum or
 * not. This generally includes all enums apart from
 * enum8/16/32 (generally defined in types.xml)
 * example for if_is_strongly_typed_chip_enum:
 * {{#if_is_strongly_typed_chip_enum type}}
 * strongly typed enum
 * {{else}}
 * not a strongly typed enum
 * {{/if_is_strongly_typed_chip_enum}}
 *
 * @param {*} type
 * @param {*} options
 * @returns Promise of content.
 */
async function if_is_strongly_typed_chip_enum(type, options) {
  // There are certain exceptions.
  if (type.toLowerCase() == 'vendor_id') {
    return options.fn(this);
  } else {
    let packageIds = await templateUtil.ensureZclPackageIds(this);
    let enumRes;
    // Retrieving the enum from the enum table
    if (type && typeof type === 'string') {
      enumRes = await queryZcl.selectEnumByName(
        this.global.db,
        type,
        packageIds
      );
    } else {
      enumRes = await queryZcl.selectEnumById(this.global.db, type);
    }

    // Checking if an enum is atomic. If an enum is not atomic then the enum
    // is a strongly typed enum
    if (enumRes) {
      let a = await queryZcl.selectAtomicType(
        this.global.db,
        packageIds,
        enumRes.name
      );
      if (a) {
        // if an enum has an atomic type that means it's a weakly-typed enum.
        return options.inverse(this);
      } else {
        return options.fn(this);
      }
    }
    return options.inverse(this);
  }
}

/**
 * Checks whether a type is an enum for purposes of its chipType.  That includes
 * both spec-defined enum types and types that we map to enum types in our code.
 */
async function if_chip_enum(type, options) {
  if (type.toLowerCase() == 'vendor_id') {
    return options.fn(this);
  }

  let pkgIds = await templateUtil.ensureZclPackageIds(this);
  let checkResult = await zclHelper.isEnum(this.global.db, type, pkgIds);
  let result;
  if (checkResult != 'unknown') {
    result = options.fn(this);
  } else {
    result = options.inverse(this);
  }
  return templateUtil.templatePromise(this.global, result);
}

async function if_chip_complex(options) {
  // `zcl_command_arguments` has an `isArray` property and `type`
  // contains the array element type.
  if (this.isArray) {
    return options.fn(this);
  }

  // zcl_attributes iterators does not expose an `isArray` property
  // and `entryType` contains the array element type, while `type`
  // contains the atomic type, which is array in this case.
  // https://github.com/project-chip/zap/issues/412
  if (this.type == 'array') {
    return options.fn(this);
  }

  let pkgIds = await templateUtil.ensureZclPackageIds(this);
  let checkResult = await zclHelper.isStruct(this.global.db, this.type, pkgIds);
  let result;
  if (checkResult != 'unknown') {
    result = options.fn(this);
  } else {
    result = options.inverse(this);
  }
  return templateUtil.templatePromise(this.global, result);
}

async function chip_access_elements(options) {
  // console.log(options);
  let entityType = options.hash.entity;

  if (entityType == null) {
    throw new Error(
      'Access helper requires entityType, either from context, or from the entity="<entityType>" option.'
    );
  }

  let accessList = null;

  // Exaples of operations:
  //   { operation: null, role: null, accessModifier: 'fabric-scoped' },
  //   { operation: 'read', role: 'administer', accessModifier: null },
  //   { operation: 'write', role: 'administer', accessModifier: null }
  //
  // Note the existence of a null operation with a modifier of fabric-scoped

  // accessDefaults contains acceptable operations
  // together with their default value
  let accessDefaults = new Map();

  switch (entityType) {
    case 'attribute':
      accessList = await queryAccess.selectAttributeAccess(
        this.global.db,
        this.id
      );
      accessDefaults.set('read', 'view');
      accessDefaults.set('write', 'operate');
      break;
    case 'command':
      accessList = await queryAccess.selectCommandAccess(
        this.global.db,
        this.id
      );
      accessDefaults.set('invoke', 'operate');
      break;
    case 'event':
      accessList = await queryAccess.selectEventAccess(this.global.db, this.id);
      accessDefaults.set('read', 'view');
      break;
    default:
      throw new Error(
        `Entity type ${entityType} not supported. Requires: attribute/command/event.`
      );
  }

  let accessEntries = [];

  for (let element of accessList) {
    if (!element.operation) {
      continue; // not a valid operation (likely null)
    }

    const operation = element.operation.toLowerCase();
    if (!accessDefaults.has(operation)) {
      continue; // not a valid operation (may be a bug or non-matter operation)
    }

    const role = element.role.toLowerCase();

    if (role === accessDefaults.get(operation)) {
      continue; // already set as a default
    }

    accessEntries.push({ operation, role });
  }

  let p = templateUtil.collectBlocks(accessEntries, options, this);
  return templateUtil.templatePromise(this.global, p);
}

//
// Module exports
//
exports.chip_clusters = chip_clusters;
exports.chip_has_clusters = chip_has_clusters;
exports.chip_client_clusters = chip_client_clusters;
exports.chip_has_client_clusters = chip_has_client_clusters;
exports.chip_server_clusters = chip_server_clusters;
exports.chip_has_server_clusters = chip_has_server_clusters;
exports.chip_cluster_commands = chip_cluster_commands;
exports.chip_cluster_command_arguments = chip_cluster_command_arguments;
exports.chip_cluster_command_arguments_with_structs_expanded =
  chip_cluster_command_arguments_with_structs_expanded;
exports.chip_server_global_responses = chip_server_global_responses;
exports.chip_cluster_responses = chip_cluster_responses;
exports.chip_cluster_response_arguments = chip_cluster_response_arguments;
exports.chip_attribute_list_entryTypes = chip_attribute_list_entryTypes;
exports.chip_server_cluster_attributes = chip_server_cluster_attributes;
exports.chip_server_cluster_events = chip_server_cluster_events;
exports.chip_server_has_list_attributes = chip_server_has_list_attributes;
exports.chip_server_has_reportable_attributes =
  chip_server_has_reportable_attributes;
exports.chip_available_cluster_commands = chip_available_cluster_commands;
exports.chip_endpoints = chip_endpoints;
exports.chip_endpoint_clusters = chip_endpoint_clusters;
exports.if_chip_enum = if_chip_enum;
exports.if_chip_complex = if_chip_complex;
exports.if_basic_global_response = if_basic_global_response;
exports.chip_cluster_specific_structs = chip_cluster_specific_structs;
exports.chip_shared_structs = chip_shared_structs;
exports.chip_access_elements = chip_access_elements;
exports.if_is_strongly_typed_chip_enum = if_is_strongly_typed_chip_enum;
exports.if_is_strongly_typed_bitmap = if_is_strongly_typed_bitmap;

exports.meta = {
  category: dbEnum.helperCategory.matter,
  alias: ['templates/chip/helper.js', 'matter-chip-helper'],
};
